package com.fqy.ticket.common.aspect;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

/**
 * Spring AOP 日志切面类
 * 功能：拦截Controller层方法，记录请求信息、参数、返回结果及执行耗时
 * 机制：通过@Aspect声明为切面，@Component由Spring容器自动扫描并注册为Bean
 */
@Aspect
@Component
public class LogAspect {

    /**
     * Spring容器初始化时自动调用的构造函数
     * 说明：当Spring扫描到@Component注解时，会通过反射创建LogAspect实例，此构造函数由容器自动调用
     * 作用：通常用于初始化资源或调试（如打印实例创建日志）
     */
    public LogAspect() {
        System.out.println("Common LogAspect（由Spring容器自动实例化）");
    }

    /**
     * SLF4J日志记录器（线程安全）
     * 说明：通过LoggerFactory获取与当前类绑定的日志实例，用于输出结构化日志
     */
    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 定义切入点表达式（匹配需要拦截的方法）
     * 语法规则（AspectJ）：execution(修饰符? 返回类型 包路径 类名 方法名(参数) 异常?)
     * 具体含义：
     * - execution：匹配方法执行连接点（最常用的切入点指示符）
     * - public：仅匹配public修饰的方法（排除protected/private方法）
     * - *：通配符，此处表示任意返回类型（如String、void、User等）
     * - com.fqy..：递归匹配com.fqy（如com.fqy.user、com.fqy.admin.controller）
     * - *Controller：匹配所有类名以"Controller"结尾的类（如UserController、OrderController）
     * - *(..)：第一个*匹配任意方法名，(..)表示任意参数列表（包括无参）
     * 效果：拦截com.jiawa包下所有Controller类的public方法
     */
    @Pointcut("execution(public * com.fqy..*Controller.*(..))")
    public void controllerPointcut() {
        // 切入点方法：仅用于声明切入点表达式，无实际逻辑
    }

    /**
     * 前置通知（Before Advice）
     * 触发时机：目标方法执行前（如Controller接口被调用时）
     * 功能：记录请求基本信息（URL、方法、客户端IP）和过滤后的请求参数
     * @param joinPoint 由Spring AOP框架自动注入的连接点对象，包含被拦截方法的上下文信息
     *                  （如方法参数、目标对象、方法签名等）
     */
    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) {

        /* -------------------- 获取请求上下文 -------------------- */
        // Spring通过ThreadLocal存储当前线程的请求上下文（ServletRequestAttributes）
        // 原理：当HTTP请求进入DispatcherServlet时，会将请求信息绑定到当前线程的ThreadLocal中
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        // 注意：非HTTP环境（如定时任务）中attributes可能为null，需做空值检查
        if (attributes == null) {
            LOG.debug("当前非HTTP请求环境，跳过请求日志记录");
            return;
        }

        // 从上下文获取HTTP请求对象（包含URL、方法、头信息等）
        HttpServletRequest request = attributes.getRequest();

        /* -------------------- 获取方法元信息 -------------------- */
        // 获取被拦截方法的签名（包含类名、方法名、参数类型等信息）
        org.aspectj.lang.Signature signature = joinPoint.getSignature();
        // 类名（如com.jiawa.controller.UserController）
        String className = signature.getDeclaringTypeName();
        // 方法名（如getUserById）
        String methodName = signature.getName();

        /* -------------------- 打印请求基础信息 -------------------- */
        LOG.info("------------- 请求日志开始 -------------");
        LOG.info("请求地址: {} {}",
                request.getRequestURL().toString(),  // 请求完整URL（如http://localhost:8080/user/get）
                request.getMethod()                  // HTTP方法（如GET/POST）
        );
        LOG.info("被调方法: {}.{}()", className, methodName);  // 类名.方法名（如com.jiawa.UserController.getUser()）
        LOG.info("客户端IP: {}", request.getRemoteAddr());     // 客户端IP地址（如192.168.1.100）

        /* -------------------- 处理并记录请求参数 -------------------- */
        // 获取方法参数列表（可能包含无法序列化的特殊类型）
        Object[] args = joinPoint.getArgs();

        // 过滤一些特殊类型
        // 需排除的类型说明：
        // - ServletRequest/ServletResponse：包含IO流等资源，无法序列化
        // - MultipartFile：文件上传对象，直接序列化无意义（应记录文件名即可）
        Object[] filteredArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg instanceof ServletRequest
                    || arg instanceof ServletResponse
                    || arg instanceof MultipartFile) {
                // 跳过不可序列化的参数（用null占位，避免数组索引错位）
                filteredArgs[i] = null;
                continue;
            }
            filteredArgs[i] = arg;
        }

        // 敏感字段过滤（需根据业务配置）
        // 示例：{"idCard", "phone", "password", "bankAccount"}
        String[] sensitiveFields = {};  // 实际使用时填充敏感字段名

        // 使用FastJSON的属性过滤器排除敏感字段
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
        excludeFilter.addExcludes(sensitiveFields);  // 添加需要排除的字段名

        // 序列化并记录过滤后的参数（避免敏感信息泄露）
        LOG.info("请求参数: {}", JSONObject.toJSONString(filteredArgs, excludeFilter));
    }

    /**
     * 环绕通知（Around Advice）
     * 触发时机：目标方法执行前后（包裹目标方法的执行）
     * 功能：记录方法执行耗时和返回结果
     * @param proceedingJoinPoint 特殊的JoinPoint，包含执行目标方法的能力（proceed()方法）
     * @return 目标方法的返回值（需原样返回，否则会覆盖实际结果）
     * @throws Throwable 目标方法可能抛出的异常（需向上抛出）
     */
    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 记录方法开始执行时间（用于计算耗时）
        long startTime = System.currentTimeMillis();

        try {
            // 执行目标方法（关键步骤：必须调用proceed()才能触发实际方法执行）
            Object result = proceedingJoinPoint.proceed();

            /* -------------------- 处理并记录返回结果 -------------------- */
            // 敏感字段过滤（与doBefore方法逻辑一致）
            String[] sensitiveFields = {};  // 实际使用时填充敏感字段名
            PropertyPreFilters filters = new PropertyPreFilters();
            PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
            excludeFilter.addExcludes(sensitiveFields);

            // 序列化并记录过滤后的返回结果（避免敏感信息泄露）
            LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludeFilter));

            return result;
        } finally {
            // 记录方法执行耗时（无论方法是否异常都会执行）
            long costTime = System.currentTimeMillis() - startTime;
            LOG.info("------------- 请求日志结束 耗时：{}ms -------------", costTime);
        }
    }
}